odoo V10中文参考手册(八:Javascript)

Javascript

Widgets

web.Widget输出class Widget(),是所有可视组件的基类,相当于mvc的view层,提供一系列的处理页面的方法

  • 处理多widget之间的继承、被继承关系
  • 提供可扩展的生命周期安全管理(当父类被destruct时自动将对应子类清除)
  • 自动使用qweb引擎渲染
  • 与backbone兼容的快捷方法

DOM根元素

Widget()负责的是根DOM下的一部分widget页面,可以通过两个属性来获取widget的DOM:

  • Widget.el - widget对应的原始根DOM
  • Widget.$el - 使用jQuery选择的el

有两种方法来定义生成DOM根元素:

  • Widget.template - qweb的模板名,指定模板会在widget初始化之后、实际渲染之前渲染。该模板生成的根元素就会作为对应widget的DOM根元素
  • Widget.tagName - 当没有设置模板名的时候使用,默认是div,它会被设置成widget的DOM根元素,可以通过以下属性来自定义对应DOM根元素:
  • Widget.id - 在DOM根元素上生成一个id属性
  • Widget.className - 在DOM根元素上生成一个class属性
  • Widget.attributes - 属性映射表,会自动将里面的键值对设置为根元素的对应属性
  • 当设置了模板名,上述参数将不能使用
  • Widget.renderElement() - 可以通过此方法来渲染widget的根DOM并设置,使用的是template或tagName,并调用setElement() 来设置
  • 可以覆盖renderElement() 方法来实现自定义的渲染,但是如果没有在里面调用__super的话必须要调用setElement() 方法

使用widget

widget的生命周期分三个阶段:

  • 1.创建并初始化
    Widget.init(parent) - widget的初始化方法,可以接收更多的参数来覆盖父级widget

参数:
parent (Widget()) - 新创建的widget的父级,如果某widget没有父级可传null

  • 2.注入DOM并启动,通过调用以下方法中的一个来完成
  • Widget.appendTo(element) - 渲染widget并用jquery的appendTo添加到对应DOM最后一个后元素前
  • Widget.prependTo(element) 渲染widget并用jquery的prependTo插入到对应DOM第一个子元素前
  • Widget.insertAfter(element) - 渲染widget并用jquery的insertAfter添加到对应dom之后
  • Widget.insertBefore(element) - 渲染widget并用jquery的insertBefore添加到对应dom之前

上述方法接收的参数和对应jquery方法接收的参数一致,会返回一个 deferred延迟执行对象,并赋予三个任务:

1.使用renderElement()来渲染widget的根元素
2.用对应的jquery方法将widget插入到dom中
3.启动widget并将启动的结果返回
Widget.start():当widget被插入到DOM之后异步启动,一般用于异步的rpc调用以获取远端数据用于widget中,完成后需要返回一个deferred对象。在start方法执行完成之前widget的功能不一定是完整的。

  • 3.销毁并清除widget对象
    Widget.destroy() - 销毁它的子类,解绑所有事件,将它的根元素从DOM移除。当父widget被销毁时会自动调用,如果它没有父类 或需要将当前widget移除但保留父级widget时 就必须显示调用

与widget销毁相关的函数:

  • Widget.alive(deferred[, reject=false])
    由于RPC调用一般比较耗时,可能在它执行完成的时候widget已经被销毁了,这时在会在一个无效的widget对象上做操作,alive可用于处理rpc调用,并保证RPC调用返回后只在有效的widget上执行对应操作:
this.alive(this.model.query().all()).then(function (records) {
    // would break if executed after the widget is destroyed, wrapping
    // rpc in alive() prevents execution
    _.each(records, function (record) {
        self.$el.append(self.format(record));
    });
});

参数:
deferred - deferred对象
reject - 默认情况下如rpc调用完成后widget已被销毁的话对应的deferred对象只是被封锁了,如果设置为True的话会将其调用拒绝

  • Widget.isDestroyed()
    如果widget已经被销销毁了,会返回true,否则返回false

获取DOM内容

由于widget负责其DOM元素下的内容,可以用一个简便的方法去获取它DOM元素内的子片段:
Widget.$(selector) 将css选择器应用到widget的根DOM上

this.$(selector);
相当于this.$el.find(selector);

重置DOM根元素

Widget.setElement(element)
将widget的根DOM设置为指定的DOM,参数element需为一个DOM元素或相应的jquery对象

DOM事件处理

widget一般需要在相应页面内响应用户的动作,这需要通过将事件绑定到DOM元素上来实现。

  • Widget.events
    事件是一个事件选择器(事件名和css选择器之间以空格分开)- 回调函数的映射,回调函数可以是widget内置函数或一个函数对象,this表示相应widget
events: {
    'click p.oe_some_class a': 'some_method',
    'change input': function (e) {
        e.stopPropagation();
    }
},

回调函数只会被对应根DOM的匹配子元素触发。如果事件选择器留空的话,该事件是会被自动绑定到widget的根DOM上。

  • Widget.delegateEvents()
    该方法用于将事件绑定到DOM,当设置好widget的根dom后会自动被调用,可以通过重写它来设置比events映射表指定的更为复杂的事件,但父级方法必须被显式调用,否则events不会被处理。

  • Widget.undelegateEvents()
    用于在dom被销毁或重设时解绑events,当delegateEvents被覆盖时,它也需要进行覆盖。

该方法需要与backbone的delegateEvents相兼容

Widget子类

可以通过extend来创建Widget()的子类,并提供了一些抽象方法和具体方法用于使用。

var MyWidget = Widget.extend({
    // 渲染对象时使用的qweb模板
    template: "MyQWebTemplate",
    events: {
        // 事件绑定示例
        'click .my-button': 'handle_click',
    },

    init: function(parent) {
        this._super(parent);
        // 在渲染之前执行的内容
        // initialization
    },
    start: function() {
        var sup = this._super();
        // 渲染初始化逻辑

        // 允许多重deferred对象
        return $.when(
            // 从父类获取异步信号
            sup,
            // 返回自己的异步信号
            this.rpc(/* … */))
    }
});


##使用
// 创建实例
var my_widget = new MyWidget(this);
// 渲染并插入到dom
my_widget.appendTo(".some-div");

##销毁
my_widget.destroy();

开发规范

  • 避免使用id属性,用id会让部件重用变得很麻烦。可以使用class、dom节点或jquery来替代使用。如果一定要用的情况下,需要使用_.uniqueId()来特别声明:this.id = _.uniqueId('my-widget-')
  • 避免使用很通用的css名如content、navigation,以防止命名冲突。一般以根据它对应的部分来命名。
  • 避免使用全局选择器,因为某个组件可能在同个页面重复使用如仪表板,像$(selector) or document.querySelectorAll(selector) 可能会导致错误的操作,使用widget对应的($el)$()来选择
  • 不要认为你的部件拥有或控制它自己的$el
  • html模板和渲染需要合用qweb
  • 所有用于显示信息或处理事件的交互性质的组件必须继承自Widget() 并且使用它的api正确的实现

RPC

为了进行显示和交互,需要使用rpc与odoo服务器通信,odoo提供两种api来处理:

  • 底层基于JSON rpc与模块对应python片段通信
  • 高级别的直接odoo模块调用
    所有api都是异步调用的,所以它们都会返回deferred延期执行对象

高级别API 直接调用odoo模块

通过Model()来访问odoo的对象方法,通过call方法(来自web.Model)和 query方法(来自web.DataModel)来访问odoo服务器对象

  • call()是直接被映射到odoo服务端对象的同名方法的,用法跟odoo模型的api使用只有以下三点区别:
  • 交互是异步的,所以在rpc中是返回的deferred延期对象(它们会自己处理对应rpc调用结果)
  • 由于javascript规范没有__getattr__method_missing特性,需要指定调度rpc的方法
  • 没有池的概念,当需要用的时候就实例化model代理,而不是从另一个(如全局)中获取
var Users = new Model('res.users');

Users.call('change_password', ['oldpassword', 'newpassword'],
                  {context: some_context}).then(function (result) {
    // do something with change_password result
});
  • query()方法是搜索(odoo的search和read)的接口,返回一个Query()对象,该对象不可改变但是可以基于它创建新的query对象,添加新的属性和方法到原始对象。
Users.query(['name', 'login', 'user_email', 'signature'])
     .filter([['active', '=', true], ['company_id', '=', main_company]])
     .limit(15)
     .all().then(function (users) {
    // do work with users records
});

query在调用all()first()方法之前是不会实际执行的,这两个方法每次调用都会触发一个rpc请求。可以用来进行实时查询。

class Model(name)

  • Model.name 对象所绑定的model名
  • Model.call(method[, args][, kwargs]) 使用对应参数调用当前模型的对应方法

参数:

  1. method (String) 通过rpc调用的模型方法
  2. args (Array<>) 位置匹配的参数列表
  3. kwargs (Object<>) 传递的关键字参数
  • Model.query(fields)

参数:fields (Array<String>) 搜索时需要获取的字段列表

class odoo.web.Query(fields)

第一部分方法是读取方法,它们使用所调用对象的数据来响应rpc请求
  • odoo.web.Query.all() - 读取当前Query对象所对应的数据,返回一个deferred的数组
  • odoo.web.Query.first() - 读取当前Query对象对应的第一个 结果,如果没有结果返回null,有结果返回deferred对象
  • odoo.web.Query.count() - 获取当前Query对象可得到的记录数量
  • odoo.web.Query.group_by(grouping...) - 获取查询数据的分组,

参数: grouping (Array<String>) - 分组列表
返回: Deferred<Array<odoo.web.QueryGroup>> | null

第二部分方法是设置方法,它们会创建一个新Query对象并对相关属性进行扩展或替换
  • odoo.web.Query.context(ctx) 添指定的环境变量添加到搜索中
  • odoo.web.Query.filter(domain) 将指定的domain表达式添加到查询条件中,通过and与已存在的domain联接
  • odoo.web.Query.offset(offset) 设置查询的起始位置,会将原有的offset替换
  • odoo.web.Query.limit(limit) 设置查询的数据量,会将原来的limit替换
  • odoo.web.Query.order_by(fields…) 覆盖原来的排序规则,像Django的QuerySet.order_by一样
  • 接收多个排序字段,按重要性高到低排序,第一个优先级最高,字段以字符串提供
  • 每个字段默认是按升序,可以在字段前加-表示倒序
    与django不同,它没有用?来进行随机乱序和对关联字段排序方法

**分组聚合 **
odoo有非常强大的分组运算功能,但是它是递归的,并且第N+1层依赖于第n层所提供的数据,所以当odoo.models.Model.read_group()工作时这个api就不是那么直观。
odoo一般用Query()方法来代替 read_group()方法。

some_query.group_by(['field1', 'field2']).then(function (groups) {
    // do things with the fetched groups
});

该方法可以接受一个字段列表参数、也可以不带参数执行,无参数时直接返回null而不是deferred对象
当分组条件从其他地方来的时候,可以通过两种方法来测试:

  • 对group_by所得到的结果进行检查:
var groups;
if (groups = some_query.group_by(gby)) {
    groups.then(function (gs) {
        // groups
    });
}
// no groups
  • 使用when()将返回值强制转换为deferred对象
$.when(some_query.group_by(gby)).then(function (groups) {
    if (!groups) {
        // No grouping
    } else {
        // grouping, even if there are no groups (groups
        // itself could be an empty array)
    }
});

成功的情况下group_by返回的结果是一个 QueryGroup()数组

class odoo.web.QueryGroup() 返回分组的属性key,有以下几种
  • grouped_on -- 基于哪个字段进行分组计算
  • value -- 当前分组的grouped_on的值
  • length -- 分组内的记录数量
  • aggregates -- 分组聚合结果的 {field: value} 映射
odoo.web.QueryGroup.query([fields...]) 相当于Model.query() ,但只包含当前分组内的记录,返回一个Query对象供后续使用
odoo.web.QueryGroup.subgroups() 返回一个指向当前的子QueryGroup()的数组的deferred对象

底层API:RPC调用python程序

Session()对象(通过web.Session实例化)的rpc方法提供了一个低级别api用来直接调用python程序,该方法接收一个完整URL、一个参数key=>value映射表 作为参数,并将对应获取的结果转换成json格式

session.rpc('/web/dataset/resequence', {
    model: some_model,
    ids: array_of_ids,
    offset: 42
}).then(function (result) {
    // resequence didn't error out
}, function () {
    // an error occured during during call
});

web client

javascript模块系统

从odoo v8开始使用一套跟requirejs类似的js模块系统,它有以下优点:

  • 依赖关系可以保证按顺序加载
  • 更容易将文件分割成更小的逻辑单元
  • 没有全局变量
  • 很容易检查依赖关系,让重构变得容易很多

缺点:

  • 如果要通过odoo交互就必须用模块系统加载,因为有很多对象只在模块系统中能用
  • 不支持循环依赖

在这种模式下,通过require来导入需要的模块,并且显示声明所输出的对象。

odoo.define('addon_name.service', function (require) {
    var utils = require('web.utils');
    var Model = require('web.Model');

    // do things with utils and Model
    var something_useful = 15;
    return  {
        something_useful: something_useful,
    };
});

上面的代码创建了一个名叫addon_name.service的模块,使用odoo.define函数定义。

odoo.define函数有两个参数:
1.name - 新定义的模块名

2.function - 在里面定义该模块实际包含的内容,接收一个require参数,如果需要输出内容就需要有对应返回,require函数用于获取依赖的模块。

用javascript来导入需要的模块就声明输出内容,web客户端会自动进行加载。模块在文件中定义,一般最好一个文件对应一个模块。模块可以返回一个deferred对象,这样该模块只在deferred执行后才加载,而且模块可以被废弃,并在控制台记录对应信息:

Missing dependencies - 该模块不会出现在页面中,可能是javascript文件不在页面中或模块名有错误

Failed modules - 有javascript错误
Rejected modules - 模块返回的是一个废弃的deferred
Rejected linked modules - 该模块依赖于已废弃的模块
Non loaded modules - 模块所依赖的模块不存在或有错误

Web client结构

  • framework/文件夹包含所有底层的模块
  • web.ajax 用于处理rpc调用
  • web.core 核心模块,给出很多有用的对象如qweb,_t
  • web.Widget 包含widget类
  • web.Model 抽象化的web.ajax,用于直接调用服务端模型的方法
  • web.sessionodoo.session
  • web.utils 有用的代码段
  • web.time 时间相关函数
  • views/文件夹包含所有视图定义
  • widgets/包含独立的部件
  • js/文件夹包含一些重要的文件
  • action_manager.js ActionManager 类
  • boot.js 模块系统的入口
  • menu.js 顶级菜单的定义
  • web_client.js 部件WebClient
  • view_manager.js 包含ViewManager

还有其他两个文件:tour.js用于tour,compatibility.js用于将旧系统与新系统兼容,在这个文件中每个模块名被输出到全局变量odoo中。理论上模块可以不通过变量odoo使用。

javascript习惯

  • 在模块最前面声明所有依赖关系,一般按模块名字母顺序来排列
  • 在最后声明所有输出
  • 在模块开始时添加use strict 声明
  • 以合适的名字命名模块如:addon_name.description
  • 类名首字母大写如:ActionManager ,但其他的小写如web.ajax
  • 每个文件只定义一个模块

odoo web client测试

详见:http://www.odoo.com/documentation/10.0/reference/javascript.html#testing-in-odoo-web-client


译自odoo官方文档:http://www.odoo.com/documentation/10.0/reference/javascript.html ,不当之处欢迎批评指正。

内容发布自http://www.jianshu.com/u/6fdae8ec06bc,转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容